package protocol;

import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.zip.CRC32;

import protocol.data.*;
import virtualdatagramsocket.VirtualDatagramSocket;
import files.FileHandler;

public class Receiver extends Thread {
		private VirtualDatagramSocket receiverSocket;
		private FileHandler filehandler;
		private ArrayList<Integer> missingPackets;
		private int expectedSequenceNumber = 1;
		private boolean EOF = false;
		
		public void setEOF(boolean endOfFile) {
			EOF = endOfFile;
		}

		public boolean isEOF() {
			return EOF;
		}

		public void setExpectedSequenceNumber(int expectedSequenceNumber) {
			this.expectedSequenceNumber = expectedSequenceNumber;
		}

		public int getExpectedSequenceNumber() {
			return expectedSequenceNumber;
		}

		public void setMissingPackets(ArrayList<Integer> missingPackets) {
			this.missingPackets = missingPackets;
		}

		public ArrayList<Integer> getMissingPackets() {
			return missingPackets;
		}

		public void setFilehandler(FileHandler filehandler) {
			this.filehandler = filehandler;
		}

		public FileHandler getFilehandler() {
			return filehandler;
		}

		public void setReceiverSocket(VirtualDatagramSocket receiverSocket) {
			this.receiverSocket = receiverSocket;
		}

		public VirtualDatagramSocket getReceiverSocket() {
			return receiverSocket;
		}

		/**
		 * TODO: I didn't want to mix up the two 
		 * @throws SocketException
		 * @throws FileNotFoundException
		 */
		public Receiver(VirtualDatagramSocket receiverSocket, String filename) {
			this.setReceiverSocket(receiverSocket);
			this.setMissingPackets(new ArrayList<Integer>());
			//this.getReceiverSocket().connect(fellowAddress, fellowPort);
			try {
				this.setFilehandler(new FileHandler("toka" + filename));
				this.getReceiverSocket().setSoTimeout(600000);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (SocketException e) {
				e.printStackTrace();
			} 
			
		}
		
		private boolean checkCRC(byte[] b, long crc) {
			CRC32 crc2 = new CRC32();
			crc2.update(b);
			if(crc2.getValue() == crc) 
				return true;
			return false;
		}
		
		private boolean sendAck(int sequenceNumber) {
			Packet ack = new Packet();
			ack.setType((byte)2);
			
			ReplyPacket reply = new ReplyPacket();
			reply.setSequenceNumber(sequenceNumber);
			reply.setType((byte)1);
			ack.setData(reply);
			
			if(this.send(new DatagramPacket(ack.build(), ack.build().length))) return true;
			
			return false;
		}
		
		private boolean send(DatagramPacket p) {
			try {
				this.getReceiverSocket().send(p);
				return true;
			} catch (IOException e) {
				System.err.println("Receiver, send() : " + e.getMessage());
				return false;
			}
		}
		
		private boolean writeTo(byte[] bytes, long position) {
			try {
				this.getFilehandler().write(bytes, position);
				return true;
			} catch (IOException e) {
				System.err.println("Receiver, writeTo() : " + e.getMessage());
				return false;
			}
		}
		
		private boolean sendFailedCRC(int sequenceNumber){
			Packet ack = new Packet();
			ack.setType((byte)2);
			
			ReplyPacket reply = new ReplyPacket();
			reply.setSequenceNumber(sequenceNumber);
			reply.setType((byte)2);
			ack.setData(reply);
			
			if(this.send(new DatagramPacket(ack.build(), ack.build().length))) return true;
			
			return false;
		}
		
		private void removeInt(int i) {
			for(Integer seq : this.getMissingPackets()){
				if(seq.equals(i)){
					this.getMissingPackets().remove((Integer)i);
				    break;
				}
			}
		}
		
		private void addMissingPackets(int expected, int received) {
			for(int i = expected; i < received; i++ ){
				this.getMissingPackets().add(i);
			}
		}
		
		private void handleReceivedSeq(int sequenceNumber) {
			if(sequenceNumber == this.getExpectedSequenceNumber()){
				this.setExpectedSequenceNumber(this.getExpectedSequenceNumber()+1);
			} else if (sequenceNumber < this.getExpectedSequenceNumber()) {
				this.removeInt(sequenceNumber);
			} else if (sequenceNumber > this.getExpectedSequenceNumber()) {
				addMissingPackets(this.getExpectedSequenceNumber(), sequenceNumber);
				this.setExpectedSequenceNumber(sequenceNumber + 1);
			}
		}
		
		
		
		/**
		 * The whole send process..
		 */
		public void run() {
			DatagramPacket packet = null;
			packet = new DatagramPacket(new byte[1500], 1500);
			try {
				this.getReceiverSocket().receive(packet);
				this.getReceiverSocket().connect(packet.getAddress(),packet.getPort());
//				this.getReceiverSocket().setSoTimeout(50000);
			} catch(SocketTimeoutException ex) {
				System.out.println("First packet didn't arrive in time, quit transfer");
				return;
            } catch (SocketException ex ){	
			    System.err.println("Receiver, run() : " + ex.getMessage());
			    return;
			} catch (IOException ex) {
				System.err.println("Receiver, run() : " + ex.getMessage());
				return;
			}
			
			while(this.getReceiverSocket().isConnected()) {
					// Let's check it's the right kind of a packet
					Packet p = new Packet();
					p.parse(packet.getData());
					
					if(p.getType() == 1 ) {
						DataPacket data = (DataPacket)p.getData();
						if( data == null ) break;
						byte[] temp = new byte[data.getDataLength()];
						System.arraycopy(data.getData(), 0, temp, 0, data.getDataLength());
						if(checkCRC(temp, data.getCrc())) {
							System.out.println("Data ok, seq : " + data.getSequenceNumber());
						//Let's write the piece of data we've got into the file.
						    if(this.writeTo(temp, (long)((data.getSequenceNumber()-1)*1437))){
						//TODO Check CRC and decide if it's ok to ack...
						    	if(sendAck(data.getSequenceNumber())){
						    		if(data.getDataLength() < 1437) this.setEOF(true);
						    		handleReceivedSeq(data.getSequenceNumber());
						    	}
						    }
						} else {
							sendFailedCRC(data.getSequenceNumber());
						}

					}
				
				try{
					if(this.isEOF() && this.getMissingPackets().size() == 0) {
						System.out.println("Transfer completed!");
						return;
					}
					packet = new DatagramPacket(new byte[1500], 1500);
    				this.getReceiverSocket().receive(packet);
				}catch(SocketTimeoutException ex){
					System.out.println("Quit transfer, receiver timeout : " + ex.getMessage());
					return;
				} catch (IOException e) {
					System.out.println("Receiver run, receive : " + e.getMessage());
				}
			}
		}
		
		public void finalize() {
			if(this.getReceiverSocket() != null && this.getReceiverSocket().isConnected()) {
				this.getReceiverSocket().disconnect();
			}
			this.setReceiverSocket(null);
			try{
				if(this.getFilehandler() != null)
					this.getFilehandler().close();
			}catch (IOException ex) {
				ex.printStackTrace();
			}
			this.setFilehandler(null);
			try {
				super.finalize();
			} catch (Throwable e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	
	

}
